Shell学习笔记19-字Shell及Shell嵌套模式知识应用

GO


有些已经参加工作的人在写复杂的Shell(多个Shell相互调用)时,对Shell的执行模式及子Shell相关知识还是有些模糊不清,因此便有了这一篇的内容。这篇学习笔记就是用来说明子Shell及Shell嵌套模式知识应用。

1. 子Shell的知识及实践说明

1.1. 什么是子Shell

子Shell的本质可以理解为Shell的子进程,子进程的概念是由父进程的概念引申而来的,在Linux系统中,系统运行的应用程序几乎都是从init(PID为1的进程)进程派生而来的,所有这些应用程序都可以视为init进程的子进程,而init则为它们的父进程,通过执行pstree -a命令就可以看到init及系统中其它进程的进程树信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
[theshu@host ~]$ pstree -a
init
├─auditd
│ └─{auditd}
├─dhclient -H host -1 -q -lf /var/lib/dhclient/dhclient-eth0.leases -pf/var
├─haveged -w 1024 -v 1
├─mingetty /dev/tty1
├─mingetty /dev/tty2
├─mingetty /dev/tty3
├─mingetty /dev/tty4
├─mingetty /dev/tty5
├─mingetty /dev/tty6
├─mysqld_safe /usr/bin/mysqld_safe --datadir=/var/lib/mysql...
│ └─mysqld --basedir=/usr --datadir=/var/lib/mysql--plugin-dir=/usr/lib64
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ ├─{mysqld}
│ └─{mysqld}
├─openvpn --daemon --writepid /var/run/openvpn/openvpn.pid --cd/etc/ope
├─python /shadowsocksr/shadowsocks/server.py -s ::0 -o...
│ ├─python /shadowsocksr/shadowsocks/server.py -s ::0 -o...
│ └─python /shadowsocksr/shadowsocks/server.py -s ::0 -o...
├─qemu-kvm_ga /bin/qemu-kvm_ga
│ └─qemu-kvm_ga /bin/qemu-kvm_ga
├─rsyslogd -i /var/run/syslogd.pid -c 5
│ ├─{rsyslogd}
│ ├─{rsyslogd}
│ └─{rsyslogd}
├─sshd
│ └─sshd
│ └─sshd
│ └─bash
│ └─pstree -a
└─udevd -d

对于Shell的子进程来说,它是一个从父级Shell进程派生而来的新的Shell进程,我们将这种新的Shell进程称为这个父级Shell的子Shell。

Shell脚本是从上至下、从左至右依次执行每一行的命令及语句的,即执行完一个命令之后再执行下一个。如果在Shell脚本中遇到子脚本(即脚本嵌套),就会先执行子脚本的内容,完成后再返回父脚本继续执行父脚本内后续的命令及语句。

通常情况下,当Shell脚本执行时,会向系统内核请求启动一个新的进程,以便在该进程中执行脚本的命令及子Shell脚本。

1.2. 子Shell的常见产生途径及特点

常见子Shell的产生途径主要包括以下4种。

1.2.1. 带&提交后台作业

下面通过Shell脚本来实现一个由&产生的子Shell

  • 脚本如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    [root@theshu ~]# cat 1.sh
    #!/bin/bash
    parent_var="Parent" #<==定义父Shell变量parent_var并赋值标志性字符
    echo "Shell Start:ParentShell Level: $BASH_SUBSHELL"
    #<==输出父Shell层级,BASH_SUBSHELL为系统环境变量
    { #<==通过花括号包裹需要被"&"影响的命令集
    echo "SubShell Level: $BASH_SUBSHELL" #<==输出子Shell的层级,和父Shell层级对比
    sub_var="Sub" #<==定义子Shell的变量sub_var,并赋值标志性字符
    echo "sub_var=$sub_var" #<==输出子Shell的变量字符串
    echo "parent_var=$parent_var"
    #<==在子Shell中输出父Shell定义的变量,看子Shell是否可以从父Shell继承变量parent_var值
    sleep 2 #<==休息2秒,用于分辨此处的子Shell执行是不是异步的
    echo "SubShell is over." #<==输出标识代表子Shell结束
    } & #<==通过"&"产生子Shell,执行子Shell的同时跳回父Shell
    echo "Now ParentShell start again." #<==子Shell代码下面紧接着代码,目的是判断此子Shell是异步执行还是同步执行
    echo "Shell Over: ParentShell start again." #<==脚本即将结束时输出父Shell的层级
    if [ -z "$sub_var" ]; then #<==判断,如果子Shell中定义的变量sub_var为空
    echo "sub_var is not defined in ParentShell" #<==则说明子Shell变量无法被父Shell引用
    else
    echo "sub_var is defined in ParentShell " #<==否则说明子Shell变量可以被父Shell引用
    fi

在这种方式下,有以下结论:

  • 在Shell中使用&可以产生子Shell
  • &产生的子Shell可以直接引用父Shell定义的本地变量
  • &产生的子Shell中定义的变量不能被父Shell引用
  • 在Shell中使用&可以实现其他程序的多线程并发功能

1.2.2. 使用管道功能

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
parent_var="Parent" #<==定义父Shell变量parent_var并赋值标志性字符
echo "Shell Start:ParentShell Level: $BASH_SUBSHELL"
#<==输出父Shell层级,BASH_SUBSHELL为系统环境变量
echo "" | \ #<==管道、换行
{
echo "SubShell Level: $BASH_SUBSHELL" #<==输出子Shell的层级,和父Shell层级对比
sub_var="Sub" #<==定义子Shell的变量sub_var,并赋值标志性字符
echo "sub_var=$sub_var" #<==输出子Shell的变量字符串
echo "parent_var=$parent_var"
#<==在子Shell中输出父Shell定义的变量,看子Shell是否可以从父Shell继承变量parent_var值
sleep 2 #<==休息2秒,用于分辨此处的子Shell执行是不是异步的
echo "SubShell is over." #<==输出标识代表子Shell结束
}
echo "Now ParentShell start again." #<==子Shell代码下面紧接着代码,目的是判断此子Shell是异步执行还是同步执行
echo "Shell Over: ParentShell start again." #<==脚本即将结束时输出父Shell的层级
if [ -z "$sub_var" ]; then #<==判断,如果子Shell中定义的变量sub_var为空
echo "sub_var is not defined in ParentShell"
else
echo "sub_var is defined in ParentShell "
fi

在这种方式下,有以下结论:

  • 在Shell中使用管道可以产生子Shell
  • 由管道产生的子Shell可以直接引用父Shell定义的本地变量
  • 由管道产生的子Shell中定义的变量不能被父Shell引用
  • 由管道产生的子Shell不能异步执行,只能在执行完毕后才能返回到父Shell环境

1.2.3. 使用()功能

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
parent_var="Parent"
echo "Shell Start:ParentShell Level: $BASH_SUBSHELL"
( #<==使用小括号,将命令集括起来
echo "SubShell Level: $BASH_SUBSHELL"
sub_var="Sub"
echo "sub_var=$sub_var"
echo "parent_var=$parent_var"
sleep 2
echo "SubShell is over."
) #<==小括号结束
echo "Now ParentShell start again."
echo "Shell Over: ParentShell start again."
if [ -z "$sub_var" ]; then
echo "sub_var is not defined in ParentShell"
else
echo "sub_var is defined in ParentShell "
fi

在这种方式下,有以下结论:

  • 在Shell中使用()可以产生子Shell
  • ()产生的子Shell可以直接引用父Shell定义的本地变量
  • ()产生的子Shell中定义的变量不能被父Shell引用
  • ()产生的子Shell不能异步执行,只有在执行完毕后才能返回到父Shell环境

1.2.4. 通过调用外部Shell脚本产生子Shell

在这种方式下,有以下结论:

  • 调用外部Shell脚本产生的子Shell可以直接引用父Shell定义的环境变量
  • 调用外部Shell脚本产生的子Shell中定义的变量(或者环境变量)不能被父Shell引用
  • 调用外部Shell脚本产生的子Shell不能异步执行,只有执行完毕后才能返回到父Shell环境

2. 子Shell在企业应用中的“坑”

在实际生产场景中,经常会遇到不经意间使用了子Shell而又没注意到,因此引发一些不该出现的”坑”的情况。

2.1. 使用管道与while循环时遭遇的“坑”

在实际工作中经常会遇到处理多行文本的需求,在Shell中,处理多行文本时使用while循环会比较方便。

例如,在subshell.sh中创建一个函数,函数里使用while read line逐行读取/etc/passwd文件的内容,并将每一行的内容逐一赋值给一个数组,最终输出数组下标为0的内容。同时如果处理过程中遇到了空行,就立即退出循环,并返回值1,否则返回默认值0。

首先来看一下/etc/passwd里的内容,可以看到文件里有行数内容,且其中有一行为空:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@theshu ~]# head /etc/passwd > /tmp/passwd
[root@theshu ~]# sed -i '5i \ ' /tmp/passwd
[root@theshu ~]# cat /tmp/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin

接下来执行过程的主体实现脚本,这其实就是一个有坑的脚本,而你可能会浑然不觉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@theshu ~]# cat test.sh
#!/bin/bash
function readPasswd() {
local retval=0
local count=0
cat /tmp/passwd | while read line #<==使用管道和while
do
array[$count]="$line"
if [ -z "${array[$count]}" ]; then
retval=1 && break
fi
((count++))
done
return $retval
}
function main(){
readPasswd
echo "retval = $?"
echo "array[0] = ${array[0]}"
}
main

执行结果如下:

1
2
3
[root@theshu ~]# sh test.sh
retval = 0
array[0] =

可以看到,执行结果和预期结果不通,为什么得不到预期的结果呢?

查看/tmp/passwd文件的内容,发现其中一行为空,但readPasswd函数中对空行做了特殊的判断和处理,即如果发现空行,则将retval的值赋为1,且退出while循环,所以readPasswd的最终返回值应该为1,而实际打印输出的retval最终值却为0,且/tmp/passwd文件的第一行是非空行,因此,不可能是把空行赋给了第一个数组元素array[0],但实际结果是打印array[0]时却得到了空行。

那么问题到底出在哪里呢?其实就是while read line那一行开头使用了管道。使用管道会产生子Shell,子Shell的特性之一就是在子Shell中定义的变量无法被父Shell引用,即不能被radPasswd这个函数所引用,而array[0]=xxxx和retval=1的赋值随即失效,函数主题继续保持retval=0的原始赋值。而array数组仅在子Shell中定义、赋值过,所以当退回到函数主题这个父Shell时,array数组被父Shell认为从未定义过,因此打印输出为空。

2.2. 解决while循环遭遇的“坑”

下面换一种方法,不适用管道来实现和上述脚本同样的功能,脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@theshu ~]# cat 20_1_6.sh
#!/bin/bash
function readPasswd() {
local retval=0
local count=0
while read line #<==用while循环读取文件,弃用管道
do
array[$count]="$line"
if [ -z "${array[$count]}" ]; then
retval=1 && break
fi
((count++))
done < /tmp/passwd #<==使用输入重定向的方法读入文件
return $retval
}
function main(){
readPasswd
echo "retval = $?"
echo "array[0] = ${array[0]}"
}
main

执行结果如下:

1
2
3
[root@theshu ~]# sh 20_1_6.sh
retval = 1
array[0] = root:x:0:0:root:/root:/bin/bash

这次改用了输入重定向的方法将/tmp/passwd的内容输入给read,实现对数据源的获取,而这一次的脚本执行结果与上一次截然不同,retval输出的值为1,打印输出array[0]的内容也和/tmp/passwd的第一行内容相同,执行结果和预期结果完全一致。

3. Shell调用脚本的模式说明

随着Shell脚本的广泛应用,它所使用的场合也变得多种多样,并且编写脚本的代码量也有所增加,有可能一个脚本的代码量可能会达到上百行甚至更多,且多种功能会被整合在一个脚本中,但是企业里的系统架构往往非常庞大,系统应用程序种类繁多,如果用一个复杂的大脚本控制多个程序,显然效果不太理想,也不符合程序架构解耦的发展趋势。因此,对于每个不同的程序,编写不同的脚本进行控制管理就是运维人员常用的方式了,即通过一个主脚本(父脚本)对所有其它功能性的脚本进行统一调用,这就产生了嵌套脚本(子Shell脚本的一种)的概念。

在主脚本中嵌套脚本的方式有很多,常见的为forkexecsource三种模式,这三种调用脚本的方式还是有一定的区别,这一节将说明这三种调用脚本模式的不同。

3.1. fork模式调用脚本知识

fork模式是最普通的脚本调用方式,即直接在父脚本里面用/bin/sh /directory/script.sh来调用脚本,或者在命令行中给script.sh脚本文件设置执行权限,然后使用/directory/script.sh来调用脚本。

使用上述方式调用脚本的时候,系统会开启一个SubShell(子Shell)执行调用的脚本,SubShell执行的时候ParentShell还在,SubShell执行完毕后返回到ParentShell。最后的结论是SubShell可以从ParentShell继承环境变量,但是默认情况下SubShell中的环境变量不能带回ParentShell。

执行方式说明:

1
2
/directory/script.sh #<==对脚本赋予执行权限,直接执行脚本
/bin/sh /directory/script.sh #<==在赋予执行权限时,利用执行解释器执行脚本。

3.2. exec模式调用脚本

exec模式与fork模式调用脚本的方式不同,不需要新开一个SubShell来执行被调用的脚本。被调用的脚本与父脚本在同一个Shell内执行,但是使用exec调用一个新脚本以后,父脚本中exec执行之后的脚本内容就不会再执行了,这就是exec和source的区别。

执行方式说明:

1
exec /directory/script.sh

3.3. source模式调用脚本

source模式与fork模式的区别是不会新开一个SubShell来执行被调用的脚本,而是在同一个Shell中执行,所以在被调用的脚本中声明的变量和环境变量都可以在主(父)脚本中获取和使用。

source模式与exec模式相比,最大的不同之处是使用source调用一个新脚本以后,父脚本中source命令行之后的内容在子脚本执行完毕后仍然会被执行。

执行方式:

1
2
source /directory/script.sh #<==使用source不容易被误解,而".""./"相近,容易被误解。
. /directory/script.sh #<=="."source命令的功能是等价的。

4. Shell调用脚本3中不通模式的应用场景

4.1. fork模式调用脚本的应用场景

fork模式调用脚本主要应用于常规嵌套脚本执行的场合,嵌套的脚本只是执行相应的命令操作,不会生成相应的进程信息,父脚本不需要引用嵌套的脚本内的变量及函数等信息,其次在嵌套脚本中定义的变量及函数等不会影响到父脚本中相同的信息定义。

4.2. exec模式调用脚本的应用场景

exec模式调用脚本需要应用于嵌套脚本在主脚本的末尾执行的场合,因此,这种模式的应用并不多见,并且可以被source模式完全取代。

4.3. source模式调用脚本的应用场景

source模式调用脚本是比较重要且最常用的一种嵌套方式,主要应用之一是执行嵌套脚本启动某些服务程序。

source模式调用脚本的另外一个应用就是使得嵌套脚本中的变量及函数等信息被父脚本使用,从而实现更多的业务处理。


OK

0%